﻿namespace CookieTerm
{
    using System;
    using System.IO;
    using System.Text;

    /// <summary>
    /// This class contains all UI independent control logic.
    /// </summary>
    public class Controller
    {
        #region Snapshot of the console
        private FrameBuffer videoBuffer;

        private ConsoleCell savedCursorPosition;
        private ConsoleCell cursorPosition;
        private bool showCursor;

        private AnsiColor foregroundColor;
        private AnsiColor backgroundColor;
        private byte attributes;
        #endregion

        private AnsiInterpreter interpreter;
        private Terminal terminal;
        private IWindow window;
        private Bookmark bookmark;

        /// <summary>
        /// Constructor. Call it from a UI thread, because we need to prompt user/password dialog.
        /// </summary>
        /// <param name="bookmark"></param>
        /// <param name="window"></param>
        public Controller(Bookmark bookmark, IWindow window)
        {
            this.window = window;
            this.bookmark = bookmark;
            this.interpreter = new AnsiInterpreter()
            {
                BufferSize = CookieTermAppSettings.TelnetBufferSize,
                ThrowExceptions = CookieTermAppSettings.InterpreterThrows
            };

            if (this.bookmark.Protocol == "Telnet")
            {
                this.terminal = new TelnetTerminal();
            }
            else if (this.bookmark.Protocol == "SSH")
            {
                this.terminal = new SshTerminal();
            }
            else
            {
                this.window.ShowMessageBox(
                    string.Format("Invalid protocol ({0}) in the bookmark {1}", bookmark.Protocol, bookmark.Name),
                    delegate
                    {
                        this.window.StartClosing();
                    });
                return;
            }

            // Setting up plubming
            this.window.OnConsoleInputReceived += this.OnInputReceivedFromBbsConsole;

            this.interpreter.OnAnsiCommandReceived += this.OnAnsiCommandReceivedFromAnsiInterpreter;
            this.interpreter.OnTextReceived += this.OnTextReceivedFromAnsiInterpreter;

            this.terminal.OnConnectionSucceeded += this.OnConnectionSucceededFromTerminal;
            this.terminal.OnConnectionFailed += this.OnConnectionFailedFromTerminal;
            this.terminal.OnDisconnected += this.OnDisconnectedFromTerminal;
            this.terminal.OnDataAvailable += this.OnDataAvailableFromTerminal;

            // Setting up the terminal
            this.terminal.TerminalType = this.bookmark.TerminalSettings.TerminalType;
            this.terminal.Hostname = this.bookmark.Hostname;
            this.terminal.Port = this.bookmark.Port;
            this.terminal.TerminalWidth = this.bookmark.DisplaySettings.Columns;
            this.terminal.TerminalHeight = this.bookmark.DisplaySettings.Rows;

            this.Reset(false);
        }

        public FrameBuffer VideoBuffer
        {
            get
            {
                if (this.videoBuffer == null)
                {
                    this.videoBuffer = new FrameBuffer();
                    this.videoBuffer.CreateBuffer(this.Rows, this.Columns);
                }

                return this.videoBuffer;
            }
        }

        public int CursorRow
        {
            get
            {
                return this.cursorPosition.Row;
            }

            set
            {
                this.cursorPosition.Row = value;

                if (this.cursorPosition.Row < 1)
                {
                    this.cursorPosition.Row = 1;
                }

                if (this.cursorPosition.Row > this.Rows)
                {
                    this.cursorPosition.Row = this.Rows;
                }
            }
        }

        public int CursorColumn
        {
            get
            {
                return this.cursorPosition.Column;
            }

            set
            {
                this.cursorPosition.Column = value;

                if (this.cursorPosition.Column < 1)
                {
                    this.cursorPosition.Column = 1;
                }

                if (this.cursorPosition.Column > this.Columns)
                {
                    this.cursorPosition.Column = this.Columns;
                }
            }
        }

        public bool ShowCursor
        {
            get
            {
                return this.showCursor;
            }

            set
            {
                bool original = this.showCursor;
                this.showCursor = value;
                if (original ^ value)
                {
                    this.window.InvalidateCell(this.CursorRow, this.CursorColumn);
                }
            }
        }

        public int Rows
        {
            get
            {
                return this.bookmark.DisplaySettings.Rows;
            }
        }

        public int Columns
        {
            get
            {
                return this.bookmark.DisplaySettings.Columns;
            }
        }

        public static bool IsAscii(char ch)
        {
            return (int)ch < 256;
        }

        /// <summary>
        /// Begin connecting to the host.
        /// </summary>
        public void BeginConnect()
        {
            if (this.bookmark.Protocol == "SSH")
            {
                string username, password;
                if (!this.window.PromptForUserNamePassword(out username, out password))
                {
                    this.window.StartClosing();
                    return;
                }

                (this.terminal as SshTerminal).UserName = username;
                (this.terminal as SshTerminal).Password = password;
            }

            this.window.StatusText = "Connecting...";

            if (CookieTermAppSettings.DisplayLogo)
            {
                this.ProcessInput(CookieTerm.Resources.AnsiFile.cookieTerm);
            }

            this.WriteText(string.Format("\r\n\r\nConnecting to {0} ({1})...", this.bookmark.Name, this.bookmark.Hostname));
            this.terminal.BeginConnect();
        }

        /// <summary>
        /// Process the input of an array of bytes. The bytes are passed through the ansi interpreter.
        /// </summary>
        /// <param name="bytes">Bytes to be processed</param>
        public void ProcessInput(byte[] bytes)
        {
            this.interpreter.ProcessInput(bytes);
        }

        /// <summary>
        /// Request to shutdown the connection.
        /// </summary>
        /// <param name="message">If it's not OK to shutdown, this contains the string describing the reason. UI can present it to the user.</param>
        /// <returns>True if the terminal is not connected and it's OK to shutdown.</returns>
        public bool RequestToShutdown(out string message)
        {
            if (this.terminal != null && this.terminal.Connecting)
            {
                message = string.Format("Connecting to {0}({1}), do you really want to close?", this.bookmark.Name, this.bookmark.Hostname);
                return false;
            }
            else if (this.terminal != null && this.terminal.Connected)
            {
                message = string.Format("Connected to {0}({1}), do you really want to close?", this.bookmark.Name, this.bookmark.Hostname);
                return false;
            }
            else
            {
                message = string.Empty;
                return true;
            }
        }

        /// <summary>
        /// Shutdown the controller. It's supposed to be called from an IWindow object.
        /// </summary>
        public void ShutDown()
        {
            this.window = null;
            if (this.terminal != null)
            {
                this.terminal.Close();
            }
        }

        #region Status operations
        /// <summary>
        /// Reset the status of the controller.
        /// </summary>
        /// <param name="redraw">If a redraw of the console is required.</param>
        public void Reset(bool redraw = true)
        {
            this.CursorColumn = 1;
            this.CursorRow = 1;
            this.ShowCursor = true;

            this.foregroundColor = AnsiColor.White;
            this.backgroundColor = AnsiColor.Black;
            this.attributes = 0;

            if (redraw)
            {
                this.ExecuteEraseInDisplay(2);
            }
        }

        public void WriteText(byte[] bytes)
        {
            char[] chars = this.bookmark.DisplaySettings.Encoding.GetChars(bytes);
            this.WriteText(chars);
        }

        public void WriteText(string s)
        {
            this.WriteText(s.ToCharArray());
        }

        public void WriteText(char[] chars)
        {
            int oldCursorRow = this.CursorRow;
            int oldCursorColumn = this.CursorColumn;

            for (int i = 0; i < chars.Length; i++)
            {
                char ch = chars[i];

                if (!char.IsControl(ch))
                {
                    int c = this.CursorColumn++;
                    this.SetCharAt(this.CursorRow, c, ch);
                    if (!IsAscii(ch))
                    {
                        c = this.CursorColumn++;
                        this.SetCharAt(this.CursorRow, c, (char)0);
                    }
                }
                else
                {
                    switch (ch)
                    {
                        case '\n':
                            if (this.CursorRow == this.Rows)
                            {
                                this.VideoBuffer.ScrollUp();
                                this.window.InvalidateRect(1, 1, this.Rows, this.Columns);
                            }
                            else
                            {
                                this.CursorRow++;
                            }

                            break;
                        case '\r':
                            this.CursorColumn = 1;
                            break;
                        case '\b':
                            this.CursorColumn--;
                            break;
                    }

                    this.window.InvalidateCell(oldCursorRow, oldCursorColumn);
                    this.window.InvalidateCell(this.CursorRow, this.CursorColumn);
                }
            }
        }

        public void ExecuteAnsiCommand(AnsiCommand command)
        {
            if (command is SelectGraphicRendition)
            {
                if (command.Parameters.Length == 0)
                {
                    this.ExecuteSelectGraphicRendition(0);
                }
                else
                {
                    foreach (int code in command.Parameters)
                    {
                        this.ExecuteSelectGraphicRendition(code);
                    }
                }

                return;
            }

            if (command is SaveCursorPosition)
            {
                this.savedCursorPosition = this.cursorPosition;
                return;
            }

            if (command is DeviceStatusReport)
            {
                ASCIIEncoding encoding = new ASCIIEncoding();
                byte[] status = encoding.GetBytes(string.Format("\x1b[{0};{1}R", this.CursorRow, this.CursorColumn));
                this.terminal.Send(status);
                return;
            }

            int oldCursorRow = this.CursorRow;
            int oldCursorColumn = this.CursorColumn;

            if (command is CursorUp)
            {
                this.CursorRow -= command.Parameters[0];
            }
            else if (command is CursorDown)
            {
                this.CursorRow += command.Parameters[0];
            }
            else if (command is CursorForward)
            {
                this.CursorColumn += command.Parameters[0];
            }
            else if (command is CursorBack)
            {
                this.CursorColumn -= command.Parameters[0];
            }
            else if (command is CursorNextLine)
            {
                this.CursorRow += command.Parameters[0];
                this.CursorColumn = 1;
            }
            else if (command is CursorPrevLine)
            {
                this.CursorRow -= command.Parameters[0];
                this.CursorColumn = 1;
            }
            else if (command is CursorPosition)
            {
                this.CursorRow = command.Parameters[0];
                this.CursorColumn = command.Parameters[1];
            }
            else if (command is RestoreCursorPosition)
            {
                this.CursorRow = this.savedCursorPosition.Row;
                this.CursorColumn = this.savedCursorPosition.Column;
            }
            else if (command is EraseInDisplay)
            {
                this.ExecuteEraseInDisplay(command.Parameters[0]);
            }
            else if (command is EraseInLine)
            {
                this.ExecuteEraseLine(command.Parameters[0]);
            }
            else if (command is InsertLine)
            {
                this.VideoBuffer.InsertLine(this.CursorRow);
                this.window.InvalidateRect(this.CursorRow, 1, this.Rows, this.Columns);
            }
            else
            {
                throw new NotImplementedException(string.Format("ANSI command not implemented: {0}", command));
            }

            this.window.InvalidateCell(oldCursorRow, oldCursorColumn);
            this.window.InvalidateCell(this.CursorRow, this.CursorColumn);
        }

        private void ExecuteSelectGraphicRendition(int code)
        {
            switch (code)
            {
                case 0:
                    this.foregroundColor = AnsiColor.White;
                    this.backgroundColor = AnsiColor.Black;
                    this.attributes = 0;
                    break;
                case 1:
                    this.attributes |= (byte)DisplayAttribute.Bold;
                    break;
                case 4:
                    this.attributes |= (byte)DisplayAttribute.Underline;
                    break;
                case 5:
                    this.attributes |= (byte)DisplayAttribute.Blink;
                    break;
                case 7:
                    this.attributes |= (byte)DisplayAttribute.Inverse;
                    break;
                case 30:
                case 31:
                case 32:
                case 33:
                case 34:
                case 35:
                case 36:
                case 37:
                    this.foregroundColor = (AnsiColor)(code - 30);
                    break;
                case 40:
                case 41:
                case 42:
                case 43:
                case 44:
                case 45:
                case 46:
                case 47:
                    this.backgroundColor = (AnsiColor)(code - 40);
                    break;
                default:
                    throw new NotImplementedException(string.Format("Graphic rendition code not implemented: {0}", code));
            }
        }

        private void ExecuteEraseInDisplay(int param)
        {
            switch (param)
            {
                case 0:
                    // clear from cursor to end of screen
                    for (int c = this.CursorColumn; c <= this.Columns; c++)
                    {
                        this.SetCharAt(this.CursorRow, c, ' ');
                    }

                    for (int r = this.CursorRow + 1; r <= this.Rows; r++)
                    {
                        for (int c = 1; c <= this.Columns; c++)
                        {
                            this.SetCharAt(r, c, ' ');
                        }
                    }

                    break;
                case 1:
                    // clear from cursor to beginning of screen
                    for (int c = this.CursorColumn; c >= 1; c--)
                    {
                        this.SetCharAt(this.CursorRow, c, ' ');
                    }

                    for (int r = this.CursorRow - 1; r >= 1; r--)
                    {
                        for (int c = this.Columns; c >= 1; c--)
                        {
                            this.SetCharAt(r, c, ' ');
                        }
                    }

                    break;
                case 2:
                    // clear the whole screen
                    for (int c = 1; c <= this.Columns; c++)
                    {
                        for (int r = 1; r <= this.Rows; r++)
                        {
                            this.SetCharAt(r, c, ' ');
                        }
                    }

                    break;
                default:
                    throw new InvalidDataException(string.Format("Invalid EraseInDisplay parameter: {0}", param));
            }
        }

        private void ExecuteEraseLine(int param)
        {
            switch (param)
            {
                case 0:
                    // clear from cursor to end of line
                    for (int c = this.CursorColumn; c <= this.Columns; c++)
                    {
                        this.SetCharAt(this.CursorRow, c, ' ');
                    }

                    break;
                case 1:
                    // clear from cursor to beginning of line
                    for (int c = this.CursorColumn; c >= 1; c--)
                    {
                        this.SetCharAt(this.CursorRow, c, ' ');
                    }

                    break;
                case 2:
                    for (int c = 1; c <= this.Columns; c++)
                    {
                        this.SetCharAt(this.CursorRow, c, ' ');
                    }

                    break;
                default:
                    throw new InvalidDataException(string.Format("Invalid ExecuteEraseLine parameter: {0}", param));
            }
        }

        private void SetCharAt(int row, int column, char character)
        {
            CharAttribute c;
            c.Character = character;
            c.Background = this.backgroundColor;
            c.Foreground = this.foregroundColor;
            c.Attributes = this.attributes;

            this.VideoBuffer.SetCharAttributeAt(row, column, c);
            this.window.InvalidateCell(row, column);
        }
        #endregion

        #region Event handlers for terminal
        private void OnDataAvailableFromTerminal(object sender, DataAvailableEventArgs e)
        {
            this.interpreter.ProcessInput(e.Data);
        }

        private void OnConnectionSucceededFromTerminal(object sender, EventArgs e)
        {
            this.window.StatusText = "Connected";
            this.terminal.Receive();
            this.window.ResetAntiIdle();
        }

        private void OnConnectionFailedFromTerminal(object sender, ConnectionFailedEventArgs e)
        {
            this.terminal = null;
            if (this.window != null)
            {
                this.ShowCursor = false;
                this.window.StatusText = "Connection failed";
                this.window.ShowMessageBox(
                    string.Format("Failed to connect to {0}({1})", this.bookmark.Name, this.bookmark.Hostname),
                    delegate
                    {
                        this.window.StartClosing();
                    });
            }
        }

        private void OnDisconnectedFromTerminal(object sender, EventArgs e)
        {
            this.terminal = null;
            if (this.window != null)
            {
                this.Reset();
                this.interpreter.ProcessInput(CookieTerm.Resources.AnsiFile.cookieTerm);
                this.ShowCursor = false;
                this.window.StatusText = "Disconnected";
                this.window.StartClosing();
            }
        }
        #endregion

        #region Event handlers for ANSI interpreter
        private void OnAnsiCommandReceivedFromAnsiInterpreter(object sender, AnsiCommandReceivedEventArgs e)
        {
            this.ExecuteAnsiCommand(e.Command);
        }

        private void OnTextReceivedFromAnsiInterpreter(object sender, AnsiTextReceivedEventArgs e)
        {
            this.WriteText(e.Text);
        }
        #endregion

        #region Event handlers for Console
        private void OnInputReceivedFromBbsConsole(object sender, ConsoleInputReceivedEventArgs e)
        {
            if (this.terminal != null && this.terminal.Connected)
            {
                this.terminal.Send(e.Data);
                this.window.ResetAntiIdle();
            }
        }
        #endregion
    }
}
